/* 
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 */

package org.apache.muse.tools.generator;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.wsdl.Definition;
import javax.wsdl.Import;
import javax.wsdl.Operation;
import javax.wsdl.PortType;
import javax.wsdl.Types;
import javax.wsdl.WSDLException;
import javax.wsdl.extensions.schema.Schema;
import javax.wsdl.factory.WSDLFactory;
import javax.wsdl.xml.WSDLReader;
import javax.wsdl.xml.WSDLWriter;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;

import lt.inventi.wsdlmerger.MavenPlugin.Logger;

import org.apache.muse.core.Environment;
import org.apache.muse.tools.generator.util.AbstractCommandLineApp;
import org.apache.muse.tools.generator.util.DefinitionInfo;
import org.apache.muse.tools.generator.util.MuseRuntimeException;
import org.apache.muse.tools.generator.util.WsdlEnvironment;
import org.apache.muse.util.CommandLine;
import org.apache.muse.util.messages.Messages;
import org.apache.muse.util.messages.MessagesFactory;
import org.apache.muse.util.xml.XmlUtils;
import org.apache.muse.ws.resource.metadata.MetadataDescriptor;
import org.apache.muse.ws.resource.metadata.WsrmdConstants;
import org.apache.muse.ws.resource.metadata.impl.SimpleMetadataDescriptor;
import org.apache.muse.ws.resource.metadata.impl.WsrmdUtils;
import org.apache.muse.ws.wsdl.WsdlUtils;
import org.springframework.core.task.TaskTimeoutException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;

import com.ibm.wsdl.ImportImpl;


/**
 * Utility class to merge all the capability WSDLs into a single WSDL file 
 * to represent the resource type. This class will take all of the
 * port types in the provided wsdls and make one port type. It will also
 * combine the WS-RP documents into one.
 * 
 * @author Andrew Eberbach (aeberbac)
 * @author Slava Pozdniakov (slava@inventi.lt) -- wsdl-merger-maven-plugin
 */
public class WsdlMerge extends AbstractCommandLineApp implements WsdlMergeConstants {

	private Messages _MESSAGES = MessagesFactory.get(WsdlMerge.class);
	
	private WSDLFactory _factory; 
	
	private WSDLReader _reader;
	
	private WSDLWriter _writer;

	private Logger logger;
	
	public WsdlMerge(Logger logger){
		this.logger = logger;
		try {
			_factory = WSDLFactory.newInstance();
			_reader = _factory.newWSDLReader();
			_writer = _factory.newWSDLWriter();
            _reader.setFeature(WsdlUtils.WSDL4J_VERBOSE_FLAG, false);
		} catch (Exception e) {
			throw new MuseRuntimeException("NoFactory", _MESSAGES.get("NoFactory"), e);
		}
	}

	public void run(File[] files, String outputFileName, String outputRMDFileName, boolean overwrite, String uri, String address) throws FileNotFoundException {		
		Document[] wsdls = new Document[files.length];
		MetadataDescriptor[] metadatas = new MetadataDescriptor[files.length];

		for(int i=0; i < files.length; i++) {
			Environment env = new WsdlEnvironment(files[i].getAbsoluteFile().getParentFile());
			wsdls[i] = WsdlUtils.createWSDL(env, files[i].getName(), true);
			
			//
			// wsdl4j will get angry when it tries to resolve these because we're not
			// providing the base directory since we've already read them in. So
			// we remove the imports.
			//
			WsdlUtils.removeSchemaReferences(wsdls[i].getDocumentElement());
			WsdlUtils.removeWsdlReferences(wsdls[i].getDocumentElement());
			
			if(outputRMDFileName != null) {
				metadatas[i] = getMetadataDescriptor(files[i].getName(), env, wsdls[i]);
			}
		}
		
		Definition mergedWsdl = merge(uri, wsdls, address);
					
		if(outputRMDFileName != null) {
			MetadataDescriptor mergedMetadata = merge(outputFileName, getFirstPortType(mergedWsdl).getQName(), metadatas);
			setMetadata(mergedMetadata.getName(), outputRMDFileName, mergedWsdl);
			writeRmd(outputRMDFileName, mergedMetadata, overwrite);
		}				
		
		writeWsdl(outputFileName, mergedWsdl, overwrite);	
	}

	private void setMetadata(String metadataName, String metadataLocation, Definition mergedWsdl) {
		mergedWsdl.addNamespace(WsrmdConstants.PREFIX, WsrmdConstants.NAMESPACE_URI);
		
		PortType portType = getFirstPortType(mergedWsdl);
		portType.setExtensionAttribute(WsrmdConstants.DESCRIPTOR_ATTR_QNAME, metadataName);
		portType.setExtensionAttribute(WsrmdConstants.DESCRIPTOR_LOCATION_ATTR_QNAME, metadataLocation);		
	}

	private PortType getFirstPortType(Definition wsdl) {
		return (PortType) wsdl.getPortTypes().values().iterator().next();
	}

	private MetadataDescriptor merge(String wsdlLocation, QName interfaceName, MetadataDescriptor[] metadatas) {
		SimpleMetadataDescriptor mergedMetadata = new SimpleMetadataDescriptor(METADATA_NAME, wsdlLocation, interfaceName);
		
		for(int i=0; i < metadatas.length; i++) {
			copyMetadata(metadatas[i], mergedMetadata);
		}
		
		return mergedMetadata;
	}

	private String checkOutputRMDArg(CommandLine arguments) {
		return arguments.getFlagValue(RMD_OUTPUT_FLAG);		
	}

	/**
	 * Print the version and exit.
	 * 
	 * @param arguments Command line arguments
	 */
	private void checkVersionArg(CommandLine arguments) {
		if(arguments.hasFlag(WsdlMergeConstants.VERSION_FLAG)) {
			handleMessage(getVersion());
			handleExit();
		}
	}

	/**
	 * Merge the collection of WSDLs into one document. This will
	 * try to take the portTypes and merge them along with the
	 * WS-RP documents (if they exist).
	 * 
	 * @param namespaceURI 
	 * 				The target namespace URI of the generated WSDL 
	 * 
	 * @param wsdlFragments
	 * 				The WSDL files to merge
	 * 
	 * @param address
	 * 				The address used for the location attribute on the generated
	 * 				service
	 * 		
	 * @return	The <code>Definition</code> representing the newly created merged WSDL
	 */
	private Definition merge(String namespaceURI, Document[] wsdlFragments, String address) {
		DefinitionInfo targetDefinition = new DefinitionInfo(namespaceURI);

		DefinitionInfo definition = null;
		for (int i = 0; i < wsdlFragments.length; i++) {	
			try {
				definition = new DefinitionInfo(load(wsdlFragments[i]));
			} catch (WSDLException e) {
				Object[] filler = { new Integer(i) };
				throw new MuseRuntimeException("WsdlFragmentParseFailed", _MESSAGES.get("WsdlFragmentParseFailed", filler), e);
			}
			logger.logInfo("Processing wsdl #"+(i+1));
			copyGlobalNamespaces(definition, targetDefinition);
			copyOperations(definition, targetDefinition);
			copyProperties(definition, targetDefinition);
		}
		
		targetDefinition.createBinding();
		targetDefinition.createService(address);
		return targetDefinition.getDefinition();
	}
	
	private void copyGlobalNamespaces(DefinitionInfo sourceDefinition,
			DefinitionInfo targetDefinition) {
		final String XMLNS_PREFIX = XMLConstants.XMLNS_ATTRIBUTE + ":";
		List extensibilityElements = sourceDefinition.getDefinition().getTypes().getExtensibilityElements();
		for (Object extensibilityElement: extensibilityElements){
			if (extensibilityElement instanceof Schema){
				Map globalNamespaces = sourceDefinition.getDefinition().getNamespaces();
				Schema schema = (Schema)extensibilityElement;
				//redefine global target namespace alias
				String targetNamespace = schema.getElement().getAttribute("targetNamespace");
				if (targetNamespace != null){
					for(Map.Entry globalNamespace: (Set<Map.Entry>)globalNamespaces.entrySet()){
						String globalNamespaceValue = globalNamespace.getValue().toString();
						String globalNamespaceKey = globalNamespace.getKey().toString();
						String attrToCreate = XMLNS_PREFIX+globalNamespaceKey;
						if (targetNamespace.equals(globalNamespaceValue)
								&& !schema.getElement().hasAttribute(attrToCreate)){
							schema.getElement().setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, attrToCreate, globalNamespaceValue);
							logger.logInfo("Target namespaced " + globalNamespaceValue + " redefined locally");
						}
					}
				}
				Map imports = schema.getImports();
				for (Map.Entry entry: (Set<Map.Entry>)imports.entrySet()){
					String importNamespace = entry.getKey().toString();
					//check if we have this namespace in local scheme definition
					NamedNodeMap attrs = schema.getElement().getAttributes();
					boolean found = false;
					for (int i = 0; attrs != null && i < attrs.getLength(); i++){
						String attrName = attrs.item(i).getNodeName();
						String attrValue = attrs.item(i).getNodeValue();
						if (attrName != null && attrValue != null 
							&& attrName.startsWith(XMLNS_PREFIX) && attrValue.equals(importNamespace)){
							found = true;
							break;
						}
					}
					if (!found){//namespace is not locally defined, searching it globally
						for(Map.Entry globalNamespace: (Set<Map.Entry>)globalNamespaces.entrySet()){
							String globalNamespaceValue = globalNamespace.getValue().toString();
							String globalNamespaceKey = globalNamespace.getKey().toString();
							if (importNamespace.equals(globalNamespaceValue)){
								//add namespace reference locally to scheme
								schema.getElement().setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, XMLNS_PREFIX+globalNamespaceKey, globalNamespaceValue);
								logger.logInfo("Namespace "+ globalNamespaceValue + " copied to local scope");
							}
						}
					}
				}
			}
		}
	}
	
	private void copyMetadata(MetadataDescriptor sourceMetadata, MetadataDescriptor targetMetadata) {
		if(sourceMetadata == null) {
			return;
		}

		for(Iterator i=sourceMetadata.getPropertyNames().iterator(); i.hasNext();) {
			QName nextProperty = (QName)i.next();
			if(targetMetadata.hasProperty(nextProperty)) {
				Object[] filler = { nextProperty };
				throw new MuseRuntimeException("DuplicateProperty", _MESSAGES.get("DuplicateProperty", filler));
			}
			
			try {
				addProperty(nextProperty, sourceMetadata, targetMetadata);
			} catch (Exception e) {
				Object[] filler = { nextProperty };
				throw new MuseRuntimeException("FailedCopyingProperty", _MESSAGES.get("FailedCopyingProperty", filler), e);
			}
		}
	}    

    /**
     * Copies one propert from a source MetadataDescriptor to a destination MetadataDescriptor.
     * Performs a deep-copy.
     * 
     * @param name 
     * @param source
     * @param target
     * @throws Exception
     */
    private void addProperty(QName name, MetadataDescriptor source, MetadataDescriptor target) throws Exception {
    	if(name == null) {
    		throw new NullPointerException();
    	}
    	
    	if(source == null) {
    		throw new NullPointerException();
    	}
    	
    	if(target == null) {
    		throw new NullPointerException();
    	}
    	
        String mutability = source.getMutability(name);
        String modifiability = source.getModifiability(name);
        Collection initialValues = new ArrayList(source.getInitialValues(name));
        Collection staticValues = new ArrayList(source.getStaticValues(name));
        Collection validValues = new ArrayList(source.getValidValues(name));
  
        target.addProperty(name, modifiability, mutability);
        target.setInitialValues(name, initialValues);
        target.setStaticValues(name, staticValues);
        target.setValidValues(name, validValues);
        
        //
        // ValidValueRange will only exist if ValidValues was empty
        //
        if (validValues.isEmpty())
        {
            target.setLowerBound(name, source.getLowerBound(name));
            target.setUpperBound(name, source.getUpperBound(name));
        }
        
        Iterator j = source.getExtendedMetadataNames(name).iterator();
        
        while (j.hasNext())
        {
            QName metadataName = (QName)j.next();
            String metadataValue = source.getExtendedMetadata(name, metadataName);
            target.setExtendedMetadata(name, metadataName, metadataValue);
        }
    }

	/**
	 * Copy properties (ie WS-RP Documents) from one WSDL to another.
	 * 
	 * @param sourceDefinition 
	 * 				The source WSDL
	 * 
	 * @param targetDefinition
	 * 				The destination (merged) WSDL
	 */
	private void copyProperties(DefinitionInfo sourceDefinition,
			DefinitionInfo targetDefinition) {
		Types types = sourceDefinition.getTypes();

		for (Iterator i = types.getExtensibilityElements().iterator(); i
				.hasNext();) {
			targetDefinition.addSchema((Schema) i.next());
		}

		targetDefinition.mergeProperties(sourceDefinition);
	}

	/**
	 * Copy operations (ie portType operations) from one WSDL to another.
	 * 
	 * @param sourceDefinition 
	 * 				The source WSDL
	 * 
	 * @param targetDefinition
	 * 				The destination (merged) WSDL
	 */
	private void copyOperations(DefinitionInfo sourceDefinition,
			DefinitionInfo targetDefinition) {

		for (Iterator i = sourceDefinition.getOperations().iterator(); i
				.hasNext();) {
			Operation nextOperation = (Operation) i.next();
			targetDefinition.addOperation(nextOperation);
		}
	}

	/**
	 * Given a <code>File</code> try to parse it and turn it into a
	 * <code>Definition</code>.
	 * 
	 * @param wsdl 
	 * 			The <code>File</code> to parse
	 * @param environment 
	 * 
	 * @return
	 * 			A <code>Defintion</code> if parsing was successful
	 * @throws WSDLException 
	 */
	private Definition load(Document document) throws WSDLException {
		String parent = null;
		return _reader.readWSDL(parent, document);									
	}
	
	private void writeRmd(String fileName, MetadataDescriptor metadata, boolean overwrite) {
		File metadataDestination = new File(fileName);
		if(!metadataDestination.exists() || overwrite) {
			try {
				checkParentDirectory(metadataDestination.getAbsoluteFile().getParentFile());
				Element rmdDoc = WsrmdUtils.createMetadataDocument(metadata);
				
				FileWriter fileWriter = new FileWriter(metadataDestination);
				fileWriter.write(XmlUtils.toString(rmdDoc));
				fileWriter.close();			
			} catch (Exception e) {
				Object[] filler = {metadataDestination.getAbsolutePath()};
				throw new MuseRuntimeException("FailedWritingRmd", _MESSAGES.get("FailedWritingRmd", filler), e);
			}
		} else {
			handleMessage(_MESSAGES.get("NotOverwriting", new String[] {metadataDestination.getPath()}));
		}
	}

	private void writeWsdl(String fileName, Definition mergedDefinition, boolean overwrite) {
		File wsdlDestination = new File(fileName);		
		
		if (!wsdlDestination.exists() || overwrite) {
			try {
				checkParentDirectory(wsdlDestination.getAbsoluteFile().getParentFile());
				Document wsdlDoc = _writer.getDocument(mergedDefinition);
	
				FileWriter fileWriter = new FileWriter(wsdlDestination);
				fileWriter.write(XmlUtils.toString(wsdlDoc));
				fileWriter.close();			
			} catch (Exception e) {
				Object[] filler = {wsdlDestination.getAbsolutePath()};
				throw new MuseRuntimeException("FailedWritingWsdl", _MESSAGES.get("FailedWritingWsdl", filler), e);
			}
		} else {
			handleMessage(_MESSAGES.get("NotOverwriting", new String[] {wsdlDestination.getPath()}));
		}		
	}
}